"""
Translation of raw user input into validated Python values.

The field protocol consists of the following three methods:

- ``required(self)``: Return whether a value is mandatory for the field.

- ``has_value(self, input)``: Return whether a value has been provided for the
  field in the given ``MultiDict`` of user input.  This method should return
  true even if the input value is invalid or malformed.  In brief, it should
  return whether translation will raise an exception or return a non-``None``
  value.

- ``translate(self, input)``: Transform a subset of a ``MultiDict`` into an
  atomic Python value, or raise either ``TranslationError`` or ``ErrorSet`` if
  input is malformed.

For fields that expect only one parameter, only the value with the final
matching key in the ``MultiDict`` of input is used, unless otherwise noted.
"""

import re
import hashlib
import datetime
from formosa import TranslationError, ErrorSet, Form

__all__ = ['TranslationError', 'String', 'Integer', 'Float', 'Decimal',
           'Password', 'Regex', 'Email', 'Date', 'Time', 'Choice',
           'MultiChoice', 'Boolean', 'Toggle', 'File', 'ZipCode', 'Phone',
           'SubForm']

class String(object):
    """Field for strings."""

    messages = {'not_empty': 'A value is required',
                'too_short_sing': 'Value must be at least one character',
                'too_short_pl': 'Value must be at least %d characters',
                'too_long_sing': 'Value may be no more than one character',
                'too_long_pl': 'Value may be no more than %d characters'}

    def __init__(self, param, allow_empty=False, max_length=None,
                 min_length=None, alt_messages={}):
        """Create a new field for strings.  Note the order of ``max_length``
        and ``min_length``!"""
        self.param = param
        self.allow_empty = allow_empty
        self.min_length = min_length
        self.max_length = max_length
        self.messages = dict(self.__class__.messages, **alt_messages)

    def required(self):
        """Return whether the field is mandatory."""
        return (not self.allow_empty)

    def has_value(self, input):
        """Return whether a value has been provided for the field in the given
        ``MultiDict`` of input."""
        return (input.get(self.param, '').strip() != '')

    def translate(self, input):
        """Translate the field from a ``MultiDict`` of values."""
        value = input.get(self.param, '').strip()
        if value == '':
            if not self.allow_empty:
                raise TranslationError(self.messages['not_empty'])
            else:
                return None
        if self.min_length is not None and len(value) < self.min_length:
            if self.min_length == 1:
                raise TranslationError(self.messages['too_short_sing'])
            else:
                raise TranslationError(self.messages['too_short_pl']
                                       % self.min_length)
        elif self.max_length is not None and len(value) > self.max_length:
            if self.max_length == 1:
                raise TranslationError(self.messages['too_long_sing'])
            else:
                raise TranslationError(self.messages['too_long_pl']
                                       % self.max_length)
        else:
            return value


class Integer(object):
    """Field to hold integer values."""

    messages = {'not_empty': 'A value is required',
                'malformed': 'Please enter a whole number',
                'too_small': 'Value may not be less than %s',
                'too_large': 'Value may be no greater than %s'}

    _pattern = re.compile('^-?[0-9]+$')

    def __init__(self, param, allow_empty=False, min=None, max=None,
                 alt_messages={}):
        """Create a new field for integer values."""
        self.param = param
        self.allow_empty = allow_empty
        self.min = min
        self.max = max
        self.messages = dict(self.__class__.messages, **alt_messages)

    def required(self):
        """Return whether the field is mandatory."""
        return (not self.allow_empty)

    def has_value(self, input):
        """Return whether a value has been provided for the field in the given
        ``MultiDict`` of input."""
        return (input.get(self.param, '').strip() != '')

    def translate(self, input):
        """Translate the field from a ``MultiDict`` of values."""
        value = input.get(self.param, '').strip()
        if value == '':
            if not self.allow_empty:
                raise TranslationError(self.messages['not_empty'])
            else:
                return None
        elif not self._pattern.search(value):
            raise TranslationError(self.messages['malformed'])
        value = int(value)
        if self.min is not None and value < self.min:
            raise TranslationError(self.messages['too_small'] % self.min)
        elif self.max is not None and value > self.max:
            raise TranslationError(self.messages['too_large'] % self.max)
        else:
            return value


class Float(object):
    """Field to hold floating-point values."""

    messages = {'not_empty': 'A value is required',
                'malformed': 'Please enter a number',
                'too_small_closed': 'Value may not be less than %s',
                'too_small_open': 'Value must be greater than %s',
                'too_large_closed': 'Value may be no greater than %s',
                'too_large_open': 'Value must be less than %s'}

    _pattern = re.compile('^-?(?:[0-9]*\.)?[0-9]+$')

    def __init__(self, param, allow_empty=False, min=None, max=None,
                 min_is_open=False, max_is_open=False, alt_messages={}):
        """Create a field for floating point values."""
        self.param = param
        self.allow_empty = allow_empty
        self.min = min
        self.max = max
        self.min_is_open = min_is_open
        self.max_is_open = max_is_open
        self.messages = dict(self.__class__.messages, **alt_messages)

    def required(self):
        """Return whether the field is mandatory."""
        return (not self.allow_empty)

    def has_value(self, input):
        """Return whether a value has been provided for the field in the given
        ``MultiDict`` of input."""
        return (input.get(self.param, '').strip() != '')

    def translate(self, input):
        """Translate the field from a ``MultiDict`` of values."""
        value = input.get(self.param, '').strip()
        if value == '':
            if not self.allow_empty:
                raise TranslationError(self.messages['not_empty'])
            else:
                return None
        elif not self._pattern.search(value):
            raise TranslationError(self.messages['malformed'])
        value = float(value)
        if self.min is not None:
            if self.min_is_open:
                if value <= self.min:
                    raise TranslationError(self.messages['too_small_open']
                                           % self.min)
            else:
                if value < self.min:
                    raise TranslationError(self.messages['too_small_closed']
                                           % self.min)
        if self.max is not None:
            if self.max_is_open:
                if value >= self.max:
                    raise TranslationError(self.messages['too_large_open']
                                           % self.max)
            else:
                if value > self.max:
                    raise TranslationError(self.messages['too_large_closed']
                                           % self.max)
        return value


class Decimal(object):
    """Field for arbitrary precision numbers, implemented using the
    ``decimal`` module from the standard library."""

    messages = {'not_empty': 'A value is required',
                'malformed': 'Please enter a number',
                'too_significant_sing': 'Number may have at most one significant digit',
                'too_significant_pl': 'Number may have at most %d significant digits',
                'too_precise_sing': 'At most one digit may be to the right of the decimal point',
                'too_precise_pl': 'At most %d digits may be to the right of the decimal point',
                'too_small_closed': 'Value may not be less than %s',
                'too_small_open': 'Value must be greater than %s',
                'too_large_closed': 'Value may be no greater than %s',
                'too_large_open': 'Value must be less than %s'}

    _pattern = re.compile('^-?(?:[0-9]*\.)?[0-9]+$')

    def __init__(self, param, precision, scale, allow_empty=False, min=None,
                 max=None, min_is_open=False, max_is_open=False,
                 alt_messages={}):
        """Create a new field for arbitrary precision numbers, with
        ``precision`` significant digits in the entire number, and
        ``scale`` digits to the right of the decimal point."""
        self.param = param
        self.precision = precision
        self.scale = scale
        self.allow_empty = allow_empty
        self.min = min
        self.max = max
        self.min_is_open = min_is_open
        self.max_is_open = max_is_open
        self.messages = dict(self.__class__.messages, **alt_messages)

    def required(self):
        """Return whether the field is mandatory."""
        return (not self.allow_empty)

    def has_value(self, input):
        """Return whether a value has been provided for the field in the given
        ``MultiDict`` of input."""
        return (input.get(self.param, '').strip() != '')

    def translate(self, input):
        """Translate the field from a ``MultiDict`` of values."""
        value = input.get(self.param, '').strip()
        if value == '':
            if not self.allow_empty:
                raise TranslationError(self.messages['not_empty'])
            else:
                return None
        elif not self._pattern.search(value):
            raise TranslationError(self.messages['malformed'])

        parts = value.split('.')
        l = len(parts[0])
        r = len(parts[1]) if len(parts) > 1 else 0
        if r > self.scale:
            if self.scale == 1:
                raise TranslationError(self.messages['too_precise_sing'])
            else:
                raise TranslationError(self.messages['too_precise_pl']
                                       % self.scale)
        elif l + r > self.precision:
            if self.precision == 1:
                raise TranslationError(self.messages['too_significant_sing'])
            else:
                raise TranslationError(self.messages['too_significant_pl']
                                       % self.precision)

        from decimal import Decimal
        value = Decimal(value)
        if self.min is not None:
            if self.min_is_open:
                if value <= self.min:
                    raise TranslationError(self.messages['too_small_open']
                                           % self.min)
            else:
                if value < self.min:
                    raise TranslationError(self.messages['too_small_closed']
                                           % self.min)
        if self.max is not None:
            if self.max_is_open:
                if value >= self.max:
                    raise TranslationError(self.messages['too_large_open']
                                           % self.max)
            else:
                if value > self.max:
                    raise TranslationError(self.messages['too_large_closed']
                                           % self.max)
        return value


class Password(String):
    """Field for passwords."""

    def __init__(self, param, salt, allow_empty=False, min_length=6,
                 max_length=12, algorithm=hashlib.md5, alt_messages={}):
        """Create a field for passwords.  If input is given, assert that it is
        between ``min_length`` and ``max_length`` characters in length.
        Prepend ``salt`` to the input, and hash the results with the given
        hashing algorithm from ``hashlib``."""
        super(Password, self).__init__(param, allow_empty, max_length,
                                       min_length, alt_messages)
        self.salt = salt
        self.algorithm = algorithm

    def translate(self, input):
        """Translate the field from a ``MultiDict`` of values.  Return the
        digestion from the specified hashing algorithm in hexadecimal string
        notation."""
        value = super(Password, self).translate(input)
        if value is None:
            return None
        else:
            return self.algorithm(self.salt + value).hexdigest()


class Regex(String):
    """A field that is validated by a specifiable regular expression, and
    otherwise treated as a string.

    Note that, like all other fields in this module, leading and trailing
    whitespace are stripped from input -- it is a subclass of ``String``,
    after all.  This is done before checking against the pattern, so any
    pattern that expects such whitespace will fail to match.  Also note that
    empty input is converted to ``None``, and pattern matching is skipped."""

    messages = dict(String.messages)
    messages['malformed'] = 'Please enter a valid value'

    def __init__(self, param, pattern, allow_empty=False, max_length=None,
                 min_length=None, alt_messages={}):
        """Create a new regular expression-validated field."""
        super(Regex, self).__init__(param, allow_empty, max_length, min_length,
                                    alt_messages)
        self.pattern = re.compile(pattern)

    def translate(self, input):
        """Translate the field from a ``MultiDict`` of values."""
        value = super(Regex, self).translate(input)
        if value is None:
            return value
        elif not self.pattern.search(value):
            raise TranslationError(self.messages['malformed'])
        else:
            return value


class Email(Regex):
    """A field for e-mail addresses."""

    messages = dict(Regex.messages)
    messages['malformed'] = 'Please enter a valid e-mail address'

    _pattern = re.compile('^[a-zA-Z0-9._\-+]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$')

    def __init__(self, param, allow_empty=False, max_length=None,
                 alt_messages={}):
        """Create a new field for e-mail addresses."""
        super(Email, self).__init__(param, self._pattern, allow_empty,
                                    max_length, None, alt_messages)


class Date(object):
    """Field for dates."""

    messages = {'not_empty': 'A value is required',
                'malformed': 'Please enter a valid date',
                'too_early': 'Date may be no earlier than %s',
                'too_late': 'Date may be no later than %s'}

    _pattern = re.compile('^(?P<m>\d{1,2})/(?P<d>\d{1,2})/(?P<y>\d{4})$')

    def __init__(self, param, allow_empty=False, min=None, max=None,
                 alt_messages={}):
        """Create a new field for dates."""
        self.param = param
        self.allow_empty = allow_empty
        self.min = min
        self.max = max
        self.messages = dict(self.__class__.messages, **alt_messages)

    def required(self):
        """Return whether the field is mandatory."""
        return (not self.allow_empty)

    def has_value(self, input):
        """Return whether a value has been provided for the field in the given
        ``MultiDict`` of input."""
        return (input.get(self.param, '').strip() != '')

    def translate(self, input):
        """Translate the field from a ``MultiDict`` of values."""
        value = input.get(self.param, '').strip()
        if value == '':
            if not self.allow_empty:
                raise TranslationError(self.messages['not_empty'])
            else:
                return None
        match = self._pattern.search(value)
        if not match:
            raise TranslationError(self.messages['malformed'])
        else:
            parts = match.groupdict()
        try:
            value = datetime.date(int(parts['y']), int(parts['m']),
                                  int(parts['d']))
        except ValueError:
            raise TranslationError(self.messages['malformed'])
        if self.min is not None and value < self.min:
            raise TranslationError(self.messages['too_early']
                                   % self._format_date(self.min))
        elif self.max is not None and value > self.max:
            raise TranslationError(self.messages['too_late']
                                   % self._format_date(self.max))
        else:
            return value

    def _format_date(self, date):
        return '%s %d, %d' % (date.strftime('%B'), date.day, date.year)


class Time(object):
    """Field for times on a 12-hour clock."""

    messages = {'not_empty': 'A value is required',
                'incomplete': 'Please enter a complete time',
                'malformed': 'Please enter a valid time',
                'too_early': 'Time may be no earlier than %s',
                'too_late': 'Time may be no later than %s'}

    _int_pattern = re.compile('^[0-9]+$')
    _ampm_pattern = re.compile('^(?:AM|PM)$')

    def __init__(self, hour_param, minute_param, ampm_param, allow_empty=False,
                 min=None, max=None, tzinfo=None, alt_messages={}):
        """Create a new field for times."""
        self.hour_param = hour_param
        self.minute_param = minute_param
        self.ampm_param = ampm_param
        self.allow_empty = allow_empty
        self.min = min
        self.max = max
        self.tzinfo = tzinfo
        self.messages = dict(self.__class__.messages, **alt_messages)

    def required(self):
        """Return whether the field is mandatory."""
        return (not self.allow_empty)

    def has_value(self, input):
        """Return whether a value has been provided for the field in the given
        ``MultiDict`` of input."""
        return ((input.get(self.hour_param, '').strip() != '')
                or (input.get(self.minute_param, '').strip() != '')
                or (input.get(self.ampm_param, '').strip() != ''))

    def translate(self, input):
        """Translate the field from a ``MultiDict`` of values."""
        hour = input.get(self.hour_param, '').strip()
        minute = input.get(self.minute_param, '').strip()
        ampm = input.get(self.ampm_param, '').strip()
        empty = [hour == '', minute == '', ampm == '']
        if all(empty):
            if not self.allow_empty:
                raise TranslationError(self.messages['not_empty'])
            else:
                return None
        elif any(empty):
            raise TranslationError(self.messages['incomplete'])
        elif not self._int_pattern.search(hour) \
                or not self._int_pattern.search(minute) \
                or not self._ampm_pattern.search(ampm):
            raise TranslationError(self.messages['malformed'])
        try:
            hour = int(hour)
            minute = int(minute)
        except ValueError:
            raise TranslationError(self.messages['malformed'])
        if not (1 <= hour <= 12 and 0 <= minute < 60):
            raise TranslationError(self.messages['malformed'])
        if ampm == 'AM' and hour == 12:
            hour = 0
        elif ampm == 'PM' and hour < 12:
            hour += 12
        value = datetime.time(hour, minute, tzinfo=self.tzinfo)
        if self.min is not None and value < self.min:
            raise TranslationError(self.messages['too_early']
                                   % self._format_time(self.min))
        elif self.max is not None and value > self.max:
            raise TranslationError(self.messages['too_late']
                                   % self._format_time(self.max))
        else:
            return value

    def _format_time(self, time):
        hour = time.strftime('%I')
        if hour.find('0') == 0:
            hour = hour[1]
        return hour + time.strftime(':%M %p')


class Choice(object):
    """Field for choosing an item from a set of options.

    Please note that options must be specified as strings.  Most often,
    however, choices are to be made from among sets of objects of a particular
    class as loaded from the database.  As such, it is recommended that such
    selectors be implemented as a subclass, where a one-to-one mapping between
    the primary keys of the collection's objects and a string representation
    thereof can be established, and enforced by wrapping ``translate()``."""

    messages = {'not_empty': 'A value is required',
                'invalid': 'Please make a valid selection'}

    def __init__(self, param, options, allow_empty=False, alt_messages={}):
        """Create a new field for a choice from a set of strings.

        ``options`` is an iterable of strings (with a ``set`` offering the
        best performance).  Because user input is stripped, its values should
        have no leading or trailing whitespace.  It should also not contain
        the empty string, as it is discarded from user input."""
        self.param = param
        self.options = options
        self.allow_empty = allow_empty
        self.messages = dict(self.__class__.messages, **alt_messages)

    def required(self):
        """Return whether the field is mandatory."""
        return (not self.allow_empty)

    def has_value(self, input):
        """Return whether a value has been provided for the field in the given
        ``MultiDict`` of input."""
        return (input.get(self.param, '').strip() != '')

    def translate(self, input):
        """Translate the field from a ``MultiDict`` of values."""
        value = input.get(self.param, '').strip()
        if value == '':
            if not self.allow_empty:
                raise TranslationError(self.messages['not_empty'])
            else:
                return None
        elif value not in self.options:
            raise TranslationError(self.messages['invalid'])
        else:
            return value


class MultiChoice(object):
    """Field for multiple choices from among a set of strings.

    See ``Choice`` for details."""

    messages = {'invalid': 'Please make a valid selection',
                'choose_sing': 'Please make a selection',
                'choose_pl': 'Please select %d options from the list',
                'too_few_sing': 'You must make at least one selection',
                'too_few_pl': 'You must make at least %d selections',
                'too_many_sing': 'You may make at most one selection',
                'too_many_pl': 'You may make at most %d selections'}

    def __init__(self, param, options, min_choices=1, max_choices=None,
                 alt_messages={}):
        """Create a new field for choices from a set of values.

        ``options`` is an iterable of strings (with a ``set`` offering the
        best performance).  Because user input is stripped, its values should
        have no leading or trailing whitespace.  It should also not contain
        the empty string, as it is discarded from user input.

        Assert that the user must make between ``min_choices`` and
        ``max_choices`` selections.  If ``max_choices`` is ``None``, the user
        may select any number of options."""
        self.options = options
        self.param = param
        self.min_choices = min_choices
        self.max_choices = max_choices
        self.messages = dict(self.__class__.messages, **alt_messages)

    def required(self):
        """Return whether the field is mandatory."""
        return (self.min_choices > 0)

    def has_value(self, input):
        """Return whether a value has been provided for the field in the given
        ``MultiDict`` of input."""
        return any(i.strip() for i in input.getall(self.param))

    def translate(self, input):
        """Translate the field from a ``MultiDict`` of values."""
        choices = set(i for i in (j.strip() for j in input.getall(self.param))
                      if i != '')
        invalids = choices - set(self.options)
        if invalids:
            raise TranslationError(self.messages['invalid'])
        num_choices = len(choices)
        if num_choices < self.min_choices:
            if self.min_choices == self.max_choices:
                if self.min_choices == 1:
                    raise TranslationError(self.messages['choose_sing'])
                else:
                    raise TranslationError(self.messages['choose_pl']
                                           % self.min_choices)
            elif self.min_choices == 1:
                raise TranslationError(self.messages['too_few_sing'])
            else:
                raise TranslationError(self.messages['too_few_pl']
                                       % self.min_choices)
        if self.max_choices is not None and num_choices > self.max_choices:
            if self.max_choices == 1:
                raise TranslationError(self.messages['too_many_sing'])
            else:
                raise TranslationError(self.messages['too_many_pl']
                                       % self.max_choices)
        return choices


class Boolean(Choice):
    """Field for truth values following a three-valued logic (i.e. true,
    false, and unknown or not applicable); in brief, a nullable boolean.

    True is represented by the string ``'true'``, and falseness is represented
    by ``'false'``; case is significant.  An absence of value is denoted by
    ``''`` or a missing parameter in the ``MultiDict`` of input."""

    def __init__(self, param, allow_empty=False, alt_messages={}):
        """Create a new field for nullable boolean values."""
        super(Boolean, self).__init__(param, set(['true', 'false']),
                                      allow_empty, alt_messages)

    def translate(self, input):
        """Translate the field from a ``MultiDict`` of values."""
        value = super(Boolean, self).translate(input)
        return {'true': True, 'false': False, None: None}[value]


class Toggle(object):
    """Field that has two states, either on or off.

    Falseness is represented by an absent value or an empty string; truth is
    represented by anything else.  A typical visual representation for this
    field is a single checkbox.

    Toggles are different from other fields in that an absence of value is a
    meaningful state (i.e. falseness).  The concept of requiredness is
    therefore not well-defined for them.  They also may never fail
    validation."""

    def __init__(self, param):
        """Create a new toggle field."""
        self.param = param

    def required(self):
        """Return ``True``, as it is impossible to omit a value."""
        return True

    def has_value(self, input):
        """Return whether a value has been provided for the field in the given
        ``MultiDict`` of input.

        This is another unintuitive method.  While translate() always returns
        a non-null value, it is indeed possible (and likely) for no value to
        be provided as input, which evaluates to falseness."""
        return (input.get(self.param, '').strip() != '')

    def translate(self, input):
        """Translate the field from a ``MultiDict`` of values."""
        return (input.get(self.param, '').strip() != '')


class File(object):
    """Field for uploaded files."""

    messages = {'not_empty': 'A value is required',
                'invalid_type': 'The uploaded file is of an invalid format',
                'too_big': 'File may be no larger than %s'}

    def __init__(self, param, allow_empty=False, valid_types=None,
                 max_size=2097152, alt_messages={}):
        """Create a new field for uploaded files.  If ``valid_types`` is
        given, require that any uploaded file be of a MIME type listed in the
        iterable.  Limit uploads to at most ``max_size`` bytes, unless
        ``max_size`` is ``None``."""
        self.param = param
        self.allow_empty = allow_empty
        self.valid_types = valid_types
        self.max_size = max_size
        self.messages = dict(self.__class__.messages, **alt_messages)

    def required(self):
        """Return whether the field is mandatory."""
        return (not self.allow_empty)

    def has_value(self, input):
        """Return whether a value has been provided for the field in the given
        ``MultiDict`` of input."""
        return (input.get(self.param, '') != '')

    def translate(self, input):
        """Translate the field from a ``MultiDict`` of values."""
        value = input.get(self.param, '')
        if value == '':
            if not self.allow_empty:
                raise TranslationError(self.messages['not_empty'])
            else:
                return None
        if self.valid_types:
            from mimetypes import guess_type
            if guess_type(value.filename)[0] not in self.valid_types:
                raise TranslationError(self.messages['invalid_type'])
        if self.max_size:
            if len(value.value) > self.max_size:
                raise TranslationError(self.messages['too_big']
                                       % self._format_file_size(self.max_size))
        return value

    def _format_file_size(self, bytes):
        if bytes < 1024:
            if bytes == 1:
                return '1 byte'
            else:
                return '%d bytes' % bytes
        elif bytes < 1048576:
            return '%.1f KB' % (bytes / 1024.0)
        elif bytes < 1073741824:
            return '%.1f MB' % (bytes / 1048576.0)
        else:
            return '%.1f GB' % (bytes / 1073741824.0)


class ZipCode(Regex):
    """Field for United States postal codes."""

    messages = dict(Regex.messages)
    messages['malformed'] = 'Please enter a valid five- or nine-digit ZIP code'

    def __init__(self, param, allow_empty=False, alt_messages={}):
        """Create a new field for ZIP codes."""
        super(ZipCode, self).__init__(param, re.compile('^\d{5}(?:-\d{4})?$'),
                                      allow_empty, max_length=10,
                                      alt_messages=alt_messages)


class Phone(Regex):
    """Field for United States telephone numbers."""

    messages = dict(Regex.messages)
    messages['malformed'] = 'Please enter a valid telephone number'

    def __init__(self, param, allow_empty=False, alt_messages={}):
        """Create a new field for telephone numbers."""
        super(Phone, self).__init__(param, re.compile('^\d{3}-\d{3}-\d{4}$'),
                                    allow_empty, max_length=12,
                                    alt_messages=alt_messages)


class SubForm(Form):
    """Container for a set of fields to be treated together as a single field.

    A typical use case for this field is to treat as whole a set of fields,
    where the set itself is optional but some member fields are required if
    any values are given.  This would correspond to the other end of an
    nullable foreign key in a relational database.

    While there is a certain amount of isolation from it, it should still be
    recognized that child fields belong to the same form as their parent
    field.  In particular, care must be taken when assigning names of request
    parameters to fields, as they use the same namespace as the rest of the
    form.  The recommended way to accommodate this is to introduce some sort
    of prefix for the parameter names of child fields, for example
    ``'address_street'``, ``'address_city'``, etc."""

    messages = {'required': 'Section is required'}

    def __init__(self, fields, validators=[], allow_empty=False):
        """Create a new sub-form, where ``fields`` maps names to fields,
        ``validators`` is an iterable of validation objects, and
        ``allow_empty`` indicates whether a value is required for the set of
        fields."""
        super(SubForm, self).__init__(fields, validators)
        self.allow_empty = allow_empty

    def required(self):
        """Return whether the field is mandatory."""
        return (not self.allow_empty)

    def has_value(self, input):
        """Return whether a value has been given for any of the child
        fields in the given ``MultiDict`` of input."""
        return any(i.has_value(input) for i in self.fields.itervalues())

    def translate(self, input):
        """Translate the field from a ``MultiDict`` of values.

        If the field is not required, and none of its child fields has a
        value, return ``None``.  Otherwise, process each child field.

        Raise ``TranslationError`` if the field is required but none of its
        child fields has a value, or ``ErrorSet`` if translation fails for of
        any of the child fields."""
        if not self.has_value(input):
            if self.required():
                raise TranslationError(self.messages['required'])
            else:
                return None
        else:
            return super(SubForm, self).translate(input)
